﻿#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;

namespace AI.Graph.Editor {


    public class ParameterList<T> where T : Parameter {

#region Fields

        private NodeCanvas m_canvas;
        private Action m_repaintCallback;
        private Func<T[]> m_getParameter;

        private int m_parameterSelectIndex = -1;
        private int m_parameterHoverIndex = -1;
        private int m_parameterDragIndex = -1;
        private int m_parameterNewDragIndex = -1;

        private T m_parameterToRename;
        private string m_parameterRenamedName;

        private Rect m_rect;
        private Rect m_dragIndicatorRect;
        private Rect m_viewRect;
        private Vector2 m_scrollPosition;
        private bool m_isVerticalScrollbarEnabled;

        // TODO remove old parameter?
        private Dictionary<T, bool> m_parameterFoldoutList = new Dictionary<T, bool> ();

        private GenericMenu m_contextMenu;

#endregion


#region Properties

        public event Action<T, string> onChangeParameterName;
        public event Action<T, int> onChangeParameterIndex;
        public event Func<int> onParameterAddClicked;
        public event Action<T> onParameterRemovedClicked;

        private int listLength {
            get { return GetParameters ().Length; }
        }

#endregion


        public ParameterList (Func<T[]> getParameter, NodeCanvas canvas, Action repaintCallback) {
            m_getParameter = getParameter;
            m_canvas = canvas;
            m_repaintCallback = repaintCallback;
        }


        void Repaint () {
            m_repaintCallback ();
        }


        T[] GetParameters () {
            return m_getParameter ();
        }


        /// <summary>
        /// Returns the parameter at the given index position.
        /// </summary>
        /// <param name="index">The index position.</param>
        /// <returns>Returns the parameter at the given index position.</returns>
        T GetParameter (int index) {
            return GetParameters ()[index];
        }


        /// <summary>
        /// Returns the current parameter height of the parameter with the given index.
        /// </summary>
        /// <param name="index">The parameter index</param>
        /// <returns>Returns the height of the parameter.</returns>
        float GetParameterHeight (int index) {
            T parameter = GetParameter (index);

            if (IsParameterUnfolded (parameter)) {
                return Styles.parameterHeight + 5 + (parameter.boundNodes.Length * 35);
            }

            return Styles.parameterHeight;
        }


        /// <summary>
        /// Returns the rect of the given parameter.
        /// </summary>
        /// <param name="index">The parameter index.</param>
        Rect GetParameterRect (int index) {
            float yMin = 0;
            for (int i = 0; i < listLength; i++) {
                if (i == index) break;

                yMin += GetParameterHeight (i);
            }

            return new Rect (0, yMin, m_isVerticalScrollbarEnabled ? m_rect.width - Styles.scrollbarWidth : m_rect.width - 2, GetParameterHeight (index));
        }


        /// <summary>
        /// Returns the rect of the node. 
        /// </summary>
        Rect GetNodeRect (int parameterIndex, int nodeIndex) {
            Rect rect = GetParameterRect (parameterIndex);
            Rect nodeRect = new Rect (rect.x + Styles.parameterFoldoutWidth, rect.y + Styles.parameterHeight + 5 + (nodeIndex * 35), 120, 30);
            return nodeRect;
        }


        /// <summary>
        /// Returns a rect for the name of the given parameter index.
        /// </summary>
        Rect GetParameterNameRect (int index) {
            Rect rect = GetParameterRect (index);
            rect.x += Styles.parameterFoldoutWidth;
            rect.width -= Styles.parameterFoldoutWidth;
            rect.width -= Styles.parameterTypeWidth;
            rect.height = Styles.parameterHeight;

            return rect;
        }


        /// <summary>
        /// Returns the index of the given parameter.
        /// </summary>
        /// <param name="parameter">The given parameter.</param>
        int GetParameterIndex (T parameter) {
            for (int i = 0; i < listLength; i++) {
                if (GetParameter (i) == parameter) {
                    return i;
                }
            }

            return -1;
        }


        /// <summary>
        /// Returns true if the given parameter at index position is unfolded.
        /// </summary>
        bool IsParameterUnfolded (int index) {
            return IsParameterUnfolded (GetParameter (index));
        }


        /// <summary>
        /// Returns true if the given parameter is unfolded.
        /// </summary>
        bool IsParameterUnfolded (T parameter) {
            if (m_parameterFoldoutList.ContainsKey (parameter)) {
                return m_parameterFoldoutList[parameter];
            }

            return false;
        }


        /// <summary>
        /// Set the given parameter unfolded value to the given value.
        /// </summary>
        void SetParameterUnfolded (T parameter, bool value) {
            if (m_parameterFoldoutList.ContainsKey (parameter)) {
                m_parameterFoldoutList[parameter] = value;
            }
        }


        /// <summary>
        /// Updates the foldout list.
        /// Adds new parameter if they are not exists.
        /// </summary>
        void UpdateParameterFoldoutList () {
            foreach (T parameter in GetParameters ()) {
                if (!m_parameterFoldoutList.ContainsKey (parameter)) {
                    m_parameterFoldoutList.Add (parameter, false);
                }
            }
        }


        public void OnGUI (Rect rect, bool canEditParameter) {
            m_rect = rect;

            UpdateParameterFoldoutList ();

            GUILayout.BeginArea (rect);

            DrawToolbarGUI (rect.width, canEditParameter);

            GUILayout.BeginArea (new Rect (0, Styles.toolbarHeight, rect.width, rect.height - Styles.toolbarHeight));

            if (listLength > 0) {
                DrawParameterListGUI (rect.width, rect.height, canEditParameter);
            }

            GUILayout.EndArea ();
            GUILayout.EndArea ();

            // Reset if out of rect.
            Event e = Event.current;
            if (e.rawType == EventType.MouseDown && !rect.Contains (e.mousePosition)) {
                m_parameterSelectIndex = -1;
                m_parameterHoverIndex = -1;
                m_parameterDragIndex = -1;
                m_parameterNewDragIndex = -1;
            }
        }


        void DrawToolbarGUI (float width, bool canEditParameter) {
            Rect rect = new Rect (0, 0, width, Styles.toolbarHeight);
            Rect plusRect = new Rect (rect.width - 20 - Styles.scrollbarWidth, 0, 20, rect.height);
            Rect minusRect = new Rect (plusRect.x - 20 - 1, 0, 20, rect.height);

            EditorGUI.Toolbar (rect, false, true, false, true);

            GUI.enabled = m_parameterSelectIndex >= 0 && canEditParameter;
            if (EditorGUI.Button (minusRect, "-")) {
                T parameter = GetParameter (m_parameterSelectIndex);
                onParameterRemovedClicked?.Invoke (parameter);
                m_parameterSelectIndex--;
            }

            GUI.enabled = canEditParameter;

            if (EditorGUI.Button (plusRect, "+")) {
                if (onParameterAddClicked != null) {
                    int indexInList = onParameterAddClicked ();
                    m_parameterSelectIndex = indexInList;
                    m_parameterToRename = GetParameter (indexInList);
                    m_parameterRenamedName = GetParameter (indexInList).name;

                    // jump to list end.
                    m_scrollPosition = new Vector2 (0, float.MaxValue);
                }
            }

            GUI.enabled = true;
        }


        void DrawParameterListGUI (float width, float height, bool canEditParameter) {
            Rect lastRect = GetParameterRect (listLength - 1);
            Rect scrollRect = new Rect (0, 0, width, height);
            Rect viewRect = new Rect (0, scrollRect.y, scrollRect.width, lastRect.yMax + 3);
            m_isVerticalScrollbarEnabled = EditorGUIUtility.IsVerticalScrollbarEnabled (scrollRect, viewRect);
            m_scrollPosition = GUI.BeginScrollView (scrollRect, m_scrollPosition, viewRect, false, false);

            OnHandleParameterEvent (canEditParameter);
            OnHandleParameterDrag (canEditParameter);

            for (int index = 0; index < listLength; index++) {
                Rect parameterRect = GetParameterRect (index);
                OnDrawParameterBackground (parameterRect, index, m_parameterHoverIndex == index, m_parameterSelectIndex == index);
                OnDrawParameter (parameterRect, index);
            }

            if (m_parameterDragIndex >= 0 && m_parameterDragIndex != m_parameterNewDragIndex) {
                Rect parameterRect = GetParameterRect (m_parameterNewDragIndex);
                Rect indicatorRect = new Rect (0, parameterRect.y, width, 2);
                EditorGUI.DrawSolidTexture (indicatorRect, Styles.parameterFocusColor.color);
            }

            GUI.EndScrollView ();
        }


        /// <summary>
        /// Handle the parameter events.
        /// </summary>
        void OnHandleParameterEvent (bool canEditParameter) {
            if (m_contextMenu != null) {
                m_contextMenu.ShowAsContext ();
                m_contextMenu = null;
            }

            Event e = Event.current;

            bool onParameter = false;
            bool onNode = false;

            for (int parameterIndex = 0; parameterIndex < listLength; parameterIndex++) {
                Rect rect = GetParameterRect (parameterIndex);
                Rect titleRect = new Rect (rect.x, rect.y, rect.width, Styles.parameterHeight);
                T parameter = GetParameter (parameterIndex);
                bool onParameterNode = false;

                if (canEditParameter) {
                    // rename event
                    if (parameter == m_parameterToRename) {
                        if (e.rawType == EventType.MouseDown && !rect.Contains (e.mousePosition) || e.rawType == EventType.KeyDown && e.keyCode == KeyCode.Return) {
                            onChangeParameterName?.Invoke (parameter, m_parameterRenamedName);
                            m_parameterToRename = null;
                        }

                        if (e.rawType == EventType.KeyDown && e.keyCode == KeyCode.Escape) {
                            m_parameterToRename = null;
                        }
                    }
                    else {
                        if (e.type == EventType.MouseDown && e.button == 0) {
                            if (e.clickCount == 2) {
                                if (GetParameterNameRect (parameterIndex).Contains (e.mousePosition)) {
                                    m_parameterToRename = parameter;
                                    m_parameterRenamedName = parameter.name;
                                }
                            }
                        }
                    }
                }

                if (IsParameterUnfolded (parameterIndex) && parameter.hasBoundedNodes && m_parameterDragIndex == -1) {
                    // check node hovering.
                    for (int nodeIndex = 0; nodeIndex < parameter.boundNodes.Length; nodeIndex++) {
                        Node node = parameter.boundNodes[nodeIndex];
                        Rect nodeRect = GetNodeRect (parameterIndex, nodeIndex);

                        if (nodeRect.Contains (e.mousePosition)) {
                            // mouse is on node.

                            m_canvas.SetHoveredNode (node);

                            onNode = true;
                            onParameterNode = true;

                            if (e.type == EventType.MouseDown && e.button == 0) {
                                // select node

                                m_parameterSelectIndex = -1;
                                m_parameterHoverIndex = -1;
                                m_parameterToRename = null;

                                m_canvas.SetSelectedNode (node);
                            }

                            Repaint ();
                        }
                    }
                }

                if (!onParameterNode && m_parameterDragIndex == -1) {
                    titleRect = new Rect (rect.x, rect.y, rect.width, Styles.parameterHeight);

                    if (titleRect.Contains (e.mousePosition)) {
                        // parameter is hovered
                        m_canvas.SetHoveredNodes (parameter.boundNodes);
                        m_parameterHoverIndex = parameterIndex;

                        onParameter = true;

                        Rect nameRect = new Rect (titleRect.x + Styles.parameterFoldoutWidth, titleRect.y, titleRect.width - Styles.parameterFoldoutWidth, titleRect.height);

                        if (nameRect.Contains (e.mousePosition) && e.type == EventType.MouseDown && (e.button == 0 || e.button == 1)) {
                            // select parameter
                            m_canvas.SetSelectedNodes (parameter.boundNodes);
                            m_parameterSelectIndex = parameterIndex;
                            Repaint ();

                            if (e.button == 1 && canEditParameter) {
                                m_contextMenu = new GenericMenu ();

                                m_contextMenu.AddItem (new GUIContent ("Rename"), false, () => {
                                    m_parameterToRename = parameter;
                                    m_parameterRenamedName = parameter.name;
                                });

                                m_contextMenu.AddItem (new GUIContent ("Delete"), false, () => { onParameterRemovedClicked?.Invoke (parameter); });
                            }
                        }

                        Repaint ();
                    }
                }
            }

            if (!onParameter) {
                m_parameterHoverIndex = -1;

                if (!onNode) {
                    if (e.type == EventType.MouseDown) {
                        m_parameterSelectIndex = -1;
                        m_parameterDragIndex = -1;
                        m_parameterNewDragIndex = -1;
                        m_canvas.ClearSelectedNodes ();

                        if (e.button == 1 && canEditParameter) {
                            if (onParameterAddClicked != null) {
                                m_contextMenu = new GenericMenu ();

                                m_contextMenu.AddItem (new GUIContent ("Create"), false, () => {
                                    int indexInList = onParameterAddClicked ();
                                    m_parameterSelectIndex = indexInList;
                                    m_parameterToRename = GetParameter (indexInList);
                                    m_parameterRenamedName = GetParameter (indexInList).name;

                                    // jump to list end.
                                    m_scrollPosition = new Vector2 (0, float.MaxValue);
                                });
                            }
                        }
                    }
                }

                Repaint ();
            }
        }


        /// <summary>
        /// Handle the parameter drag status.
        /// </summary>
        void OnHandleParameterDrag (bool canEditParameter) {
            if (!canEditParameter) {
                return;
            }

            Event e = Event.current;

            for (int index = 0; index < listLength; index++) {
                Rect rect = GetParameterRect (index);


                // start drag
                if (e.type == EventType.MouseDown && e.button == 0) {
                    Rect dragStartRect = new Rect (rect);
                    dragStartRect.height = Styles.parameterHeight;
                    dragStartRect.x = rect.x + Styles.parameterFoldoutWidth;
                    dragStartRect.width = rect.width - Styles.parameterFoldoutWidth;

                    if (dragStartRect.Contains (e.mousePosition)) {
                        m_parameterDragIndex = index;
                        m_parameterNewDragIndex = index;
                        Repaint ();
                    }
                }


                if (m_parameterDragIndex == index) {
                    // handle drag
                    if (e.type == EventType.MouseDrag && e.button == 0) {
                        for (int i = 0; i < listLength; i++) {
                            Rect parameterRect = GetParameterRect (i);
                            if (e.mousePosition.y >= parameterRect.yMin && e.mousePosition.y <= parameterRect.yMax) {
                                m_parameterNewDragIndex = i;
                                Repaint ();
                            }
                        }
                    }

                    // end drag
                    if (e.rawType == EventType.MouseUp && e.button == 0) {
                        int oldIndex = m_parameterDragIndex;
                        int newIndex = m_parameterNewDragIndex;

                        T parameter = GetParameter (oldIndex);
                        onChangeParameterIndex?.Invoke (parameter, newIndex);

                        m_parameterSelectIndex = newIndex;
                        m_parameterDragIndex = -1;
                        m_parameterNewDragIndex = -1;
                        Repaint ();
                        break;
                    }
                }
            }
        }


        void OnDrawParameterBackground (Rect rect, int index, bool hovered, bool selected) {
            if (index == GetParameterIndex (m_parameterToRename)) {
                return;
            }

            if (selected) {
                EditorGUI.DrawSolidTexture (new Rect (rect.x, rect.y, rect.width, Styles.parameterHeight), Styles.parameterFocusColor.color);
            }
            else if (hovered || index == m_parameterNewDragIndex) {
                EditorGUI.DrawSolidTexture (new Rect (rect.x, rect.y, rect.width, Styles.parameterHeight), Styles.parameterHoverColor.color);
            }
        }


        void OnDrawParameter (Rect rect, int index) {
            T parameter = GetParameter (index);

            GUI.SetNextControlName ("parameter rename");

            Rect foldoutRect = new Rect (rect.x, rect.y, Styles.parameterFoldoutWidth, Styles.parameterHeight);
            Rect nameRect = GetParameterNameRect (index);

            bool foldout = DrawFoldout (foldoutRect, IsParameterUnfolded (parameter));
            SetParameterUnfolded (parameter, foldout);

            if (m_parameterToRename == parameter) {
                m_parameterRenamedName = UnityEditor.EditorGUI.TextField (nameRect, m_parameterRenamedName, Styles.parameterTextFieldStyle);
                UnityEditor.EditorGUI.FocusTextInControl ("parameter rename");
            }
            else {
                UnityEditor.EditorGUI.LabelField (nameRect, parameter.name, Styles.parameterLabelStyle);
            }

            DrawParameterType (parameter, nameRect.xMax, rect.y);
            DrawParameterNodes (index, parameter);
        }


        void DrawParameterNodes (int index, T parameter) {
            if (IsParameterUnfolded (parameter)) {
                for (int i = 0; i < parameter.boundNodes.Length; i++) {
                    Rect nodeRect = GetNodeRect (index, i);
                    Rect nodeOutlineRect = new Rect (nodeRect.x - 2, nodeRect.y - 2, nodeRect.width + 4, nodeRect.height + 4);
                    Node node = parameter.boundNodes[i];

                    EditorGUI.DrawSolidTexture (nodeRect, node.colors.primaryColor3, new BorderRadius (4));
                    EditorGUI.DrawSolidTexture (nodeRect, Styles.nodeOutlineColor, new BorderWidth (1), new BorderRadius (4));
                    EditorGUI.Label (nodeRect, parameter.boundNodes[i].name, Styles.nodeTitleStyle);

                    if (m_canvas.IsNodeSelected (node)) {
                        EditorGUI.DrawSolidTexture (nodeOutlineRect, node.colors.primaryColor1, new BorderWidth (1), new BorderRadius (4));
                    }
                    else if (m_canvas.IsNodeHovered (node)) {
                        EditorGUI.DrawSolidTexture (nodeOutlineRect, node.colors.primaryColor2, new BorderWidth (1), new BorderRadius (4));
                    }
                }
            }
        }


        bool DrawFoldout (Rect rect, bool foldout) {
            Rect visualRect = new Rect (rect.center.x - 5, rect.center.y - 5, 10, 10);
            EditorGUI.DrawTexture (visualRect, foldout ? Styles.foldoutButtonOn : Styles.foldoutButtonOff, ScaleMode.ScaleToFit, Styles.foldoutArrowColor);

            if (Event.current.type == EventType.MouseDown && Event.current.button == 0) {
                if (rect.Contains (Event.current.mousePosition)) {
                    foldout = !foldout;

                    Repaint ();
                }
            }

            return foldout;
        }


        void DrawParameterType (T parameter, float x, float y) {
            if (parameter.type != null) {
                Rect typeRect = new Rect (x + 10, y, Styles.parameterTypeWidth - 10 - 10, Styles.parameterHeight);
                UnityEditor.EditorGUI.LabelField (typeRect, parameter.type.Name, Styles.parameterTypeLabelStyle);
            }
        }


        static class Styles {

            public static readonly EditorColor parameterHoverColor = new EditorColor (new Color32 (170, 170, 170, 255), new Color32 (48, 48, 48, 255));
            public static readonly EditorColor parameterFocusColor = new EditorColor (new Color32 (62, 125, 231, 255), new Color32 (62, 95, 150, 255));
            public static readonly Color foldoutArrowColor = new Color32 (104, 104, 104, 255);
            public static readonly Color nodeOutlineColor = new Color32 (10, 10, 10, 255);

            public static readonly GUIStyle parameterLabelStyle;
            public static readonly GUIStyle parameterTypeLabelStyle;
            public static readonly GUIStyle parameterTextFieldStyle;
            public static readonly GUIStyle nodeTitleStyle;

            public static readonly Texture2D foldoutButtonOn;
            public static readonly Texture2D foldoutButtonOff;

            public static readonly float toolbarHeight = 18;
            public static readonly float parameterHeight = 20;
            public static readonly float parameterFoldoutWidth = 30;
            public static readonly float parameterTypeWidth = 150;
            public static readonly float scrollbarWidth;


            static Styles () {
                parameterTextFieldStyle = new GUIStyle (EditorStyles.textField);
                parameterTextFieldStyle.alignment = TextAnchor.MiddleLeft;

                parameterLabelStyle = new GUIStyle (EditorStyles.label);
                parameterLabelStyle.alignment = TextAnchor.MiddleLeft;

                parameterTypeLabelStyle = new GUIStyle (EditorStyles.label);
                parameterTypeLabelStyle.alignment = TextAnchor.MiddleRight;

                nodeTitleStyle = new GUIStyle ();
                nodeTitleStyle.normal.textColor = Color.white;
                nodeTitleStyle.alignment = TextAnchor.MiddleCenter;

                scrollbarWidth = GUI.skin.verticalScrollbar.fixedWidth;

                foldoutButtonOn = EditorGUIUtility.LoadAsset<Texture2D> ("Textures/btn_arrow_down_10x10.png");
                foldoutButtonOff = EditorGUIUtility.LoadAsset<Texture2D> ("Textures/btn_arrow_right_10x10.png");

                SetGUIStyleColors ();

                AIGraphPreferences.onSkinChanged += skin => { SetGUIStyleColors (); };
            }


            static void SetGUIStyleColors () {
                parameterTextFieldStyle.normal.textColor = EditorGUI.textColor.color;
                parameterTextFieldStyle.focused.textColor = EditorGUI.textColor.color;

                parameterLabelStyle.normal.textColor = EditorGUI.textColor.color;
                parameterLabelStyle.focused.textColor = EditorGUI.textColor.color;

                Color typeColor = EditorGUI.textColor.color;
                typeColor.a = 0.5F;
                parameterTypeLabelStyle.normal.textColor = typeColor;
                parameterTypeLabelStyle.focused.textColor = typeColor;
            }

        }

    }


}
#endif